// Copyright (c) 2016, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

library sourcemap.html_parts;

import 'sourcemap_html_helper.dart';

class Annotation {
  final id;
  final int codeOffset;
  final String title;
  final data;

  Annotation(this.id, this.codeOffset, this.title, {this.data});
}

typedef bool AnnotationFilter(Annotation annotation);
typedef AnnotationData? AnnotationDataFunction(
  Iterable<Annotation> annotations, {
  required bool forSpan,
});
typedef LineData LineDataFunction(lineAnnotation);

bool includeAllAnnotation(Annotation annotation) => true;

class LineData {
  final String lineClass;
  final String lineNumberClass;

  const LineData({
    this.lineClass = 'line',
    this.lineNumberClass = 'lineNumber',
  });
}

class AnnotationData {
  final String tag;
  final Map<String, String?> properties;

  const AnnotationData({this.tag = 'a', this.properties = const {}});

  @override
  int get hashCode => tag.hashCode * 13 + properties.hashCode * 19;

  @override
  bool operator ==(other) {
    if (identical(this, other)) return true;
    if (other is! AnnotationData) return false;
    return tag == other.tag &&
        properties.length == other.properties.length &&
        properties.keys.every((k) => properties[k] == other.properties[k]);
  }
}

AnnotationDataFunction createAnnotationDataFunction({
  CssColorScheme colorScheme = const SingleColorScheme(),
  required ElementScheme elementScheme,
}) {
  return (Iterable<Annotation> annotations, {required bool forSpan}) {
    return getAnnotationDataFromSchemes(
      annotations,
      forSpan: forSpan,
      colorScheme: colorScheme,
      elementScheme: elementScheme,
    );
  };
}

LineData getDefaultLineData(data) => const LineData();

AnnotationData? getAnnotationDataFromSchemes(
  Iterable<Annotation> annotations, {
  required bool forSpan,
  CssColorScheme colorScheme = const SingleColorScheme(),
  ElementScheme elementScheme = const ElementScheme(),
}) {
  if (colorScheme.showLocationAsSpan != forSpan) return null;
  Map<String, String?> data = {};
  var id;
  if (annotations.length == 1) {
    Annotation annotation = annotations.single;
    id = annotation.id;
    data['style'] = colorScheme.singleLocationToCssColor(id);
    data['title'] = annotation.title;
  } else {
    id = annotations.first.id;
    List<int> ids = [];
    for (Annotation annotation in annotations) {
      ids.add(annotation.id);
    }
    data['style'] = colorScheme.multiLocationToCssColor(ids);
    data['title'] = annotations.map((l) => l.title).join(',');
  }
  if (id != null) {
    Set<int> ids = annotations.map<int>((l) => l.id).toSet();
    data['name'] = elementScheme.getName(id, ids);
    data['href'] = elementScheme.getHref(id, ids);
    data['onclick'] = elementScheme.onClick(id, ids);
    data['onmouseover'] = elementScheme.onMouseOver(id, ids);
    data['onmouseout'] = elementScheme.onMouseOut(id, ids);
    return AnnotationData(properties: data);
  }
  return null;
}

class HtmlPrintContext {
  final int? lineNoWidth;
  final bool usePre;
  final AnnotationFilter includeAnnotation;
  final AnnotationDataFunction getAnnotationData;
  final LineDataFunction getLineData;

  HtmlPrintContext({
    this.lineNoWidth,
    this.usePre = true,
    this.includeAnnotation = includeAllAnnotation,
    this.getAnnotationData = getAnnotationDataFromSchemes,
    this.getLineData = getDefaultLineData,
  });

  HtmlPrintContext from({
    int? lineNoWidth,
    bool? usePre,
    AnnotationFilter? includeAnnotation,
    AnnotationDataFunction? getAnnotationData,
    LineDataFunction? getLineData,
  }) {
    return HtmlPrintContext(
      lineNoWidth: lineNoWidth ?? this.lineNoWidth,
      usePre: usePre ?? this.usePre,
      includeAnnotation: includeAnnotation ?? this.includeAnnotation,
      getAnnotationData: getAnnotationData ?? this.getAnnotationData,
      getLineData: getLineData ?? this.getLineData,
    );
  }
}

enum HtmlPartKind { CODE, LINE, CONST, NEWLINE, TEXT, TAG, LINE_NUMBER }

abstract class HtmlPart {
  void printHtmlOn(StringBuffer buffer, HtmlPrintContext context);

  HtmlPartKind get kind;

  toJson(JsonStrategy strategy);

  static HtmlPart fromJson(json, JsonStrategy strategy) {
    if (json is String) {
      return ConstHtmlPart(json);
    } else {
      switch (HtmlPartKind.values[json['kind']]) {
        case HtmlPartKind.LINE:
          return HtmlLine.fromJson(json, strategy);
        case HtmlPartKind.CODE:
          return CodeLine.fromJson(json, strategy);
        case HtmlPartKind.CONST:
          return ConstHtmlPart.fromJson(json, strategy);
        case HtmlPartKind.NEWLINE:
          return const NewLine();
        case HtmlPartKind.TEXT:
          return HtmlText.fromJson(json, strategy);
        case HtmlPartKind.TAG:
          return TagPart.fromJson(json, strategy);
        case HtmlPartKind.LINE_NUMBER:
          return LineNumber.fromJson(json, strategy);
      }
    }
  }
}

class ConstHtmlPart implements HtmlPart {
  final String html;

  const ConstHtmlPart(this.html);

  @override
  HtmlPartKind get kind => HtmlPartKind.CONST;

  @override
  void printHtmlOn(StringBuffer buffer, HtmlPrintContext context) {
    buffer.write(html);
  }

  @override
  toJson(JsonStrategy strategy) {
    return {'kind': kind.index, 'html': html};
  }

  static ConstHtmlPart fromJson(Map json, JsonStrategy strategy) {
    return ConstHtmlPart(json['html']);
  }
}

class NewLine implements HtmlPart {
  const NewLine();

  @override
  HtmlPartKind get kind => HtmlPartKind.NEWLINE;

  @override
  void printHtmlOn(StringBuffer buffer, HtmlPrintContext context) {
    if (context.usePre) {
      buffer.write('\n');
    } else {
      buffer.write('<br/>');
    }
  }

  @override
  toJson(JsonStrategy strategy) {
    return {'kind': kind.index};
  }
}

class HtmlText implements HtmlPart {
  final String text;

  const HtmlText(this.text);

  @override
  HtmlPartKind get kind => HtmlPartKind.TEXT;

  @override
  void printHtmlOn(StringBuffer buffer, HtmlPrintContext context) {
    String escaped = escape(text);
    buffer.write(escaped);
  }

  @override
  toJson(JsonStrategy strategy) {
    return {'kind': kind.index, 'text': text};
  }

  static HtmlText fromJson(Map json, JsonStrategy strategy) {
    return HtmlText(json['text']);
  }
}

class TagPart implements HtmlPart {
  final String tag;
  final Map<String, String?> properties;
  final List<HtmlPart> content;

  TagPart(
    this.tag, {
    this.properties = const <String, String>{},
    this.content = const <HtmlPart>[],
  });

  @override
  HtmlPartKind get kind => HtmlPartKind.TAG;

  @override
  void printHtmlOn(StringBuffer buffer, HtmlPrintContext context) {
    buffer.write('<$tag');
    properties.forEach((String key, String? value) {
      if (value != null) {
        buffer.write(' $key="${value}"');
      }
    });
    buffer.write('>');
    for (HtmlPart child in content) {
      child.printHtmlOn(buffer, context);
    }
    buffer.write('</$tag>');
  }

  @override
  toJson(JsonStrategy strategy) {
    return {
      'kind': kind.index,
      'tag': tag,
      'properties': properties,
      'content': content.map((p) => p.toJson(strategy)).toList(),
    };
  }

  static TagPart fromJson(Map json, JsonStrategy strategy) {
    return TagPart(
      json['tag'],
      properties: json['properties'],
      content: json['content'].map(HtmlPart.fromJson).toList(),
    );
  }
}

class HtmlLine implements HtmlPart {
  final List<HtmlPart> htmlParts = <HtmlPart>[];

  @override
  HtmlPartKind get kind => HtmlPartKind.LINE;

  @override
  void printHtmlOn(StringBuffer htmlBuffer, HtmlPrintContext context) {
    for (HtmlPart part in htmlParts) {
      part.printHtmlOn(htmlBuffer, context);
    }
  }

  @override
  Map toJson(JsonStrategy strategy) {
    return {
      'kind': kind.index,
      'html': htmlParts.map((p) => p.toJson(strategy)).toList(),
    };
  }

  static HtmlLine fromJson(Map json, JsonStrategy strategy) {
    HtmlLine line = HtmlLine();
    json['html'].forEach(
      (part) => line.htmlParts.add(HtmlPart.fromJson(part, strategy)),
    );
    return line;
  }
}

class CodePart {
  final List<Annotation> annotations;
  final String subsequentCode;

  CodePart(this.annotations, this.subsequentCode);

  void printHtmlOn(StringBuffer buffer, HtmlPrintContext context) {
    Iterable<Annotation> included = annotations.where(
      context.includeAnnotation,
    );

    List<HtmlPart> htmlParts = <HtmlPart>[];
    if (included.isNotEmpty) {
      AnnotationData? annotationData = context.getAnnotationData(
        included,
        forSpan: false,
      );
      AnnotationData? annotationDataForSpan = context.getAnnotationData(
        included,
        forSpan: true,
      );

      String head = subsequentCode;
      String tail = '';
      if (subsequentCode.length > 1) {
        head = subsequentCode.substring(0, 1);
        tail = subsequentCode.substring(1);
      }

      if (annotationData != null && annotationDataForSpan != null) {
        htmlParts.add(
          new TagPart(
            annotationDataForSpan.tag,
            properties: annotationDataForSpan.properties,
            content: [
              TagPart(
                annotationData.tag,
                properties: annotationData.properties,
                content: [new HtmlText(head)],
              ),
              HtmlText(tail),
            ],
          ),
        );
      } else if (annotationDataForSpan != null) {
        htmlParts.add(
          new TagPart(
            annotationDataForSpan.tag,
            properties: annotationDataForSpan.properties,
            content: [new HtmlText(subsequentCode)],
          ),
        );
      } else if (annotationData != null) {
        htmlParts.add(
          new TagPart(
            annotationData.tag,
            properties: annotationData.properties,
            content: [new HtmlText(head)],
          ),
        );
        htmlParts.add(new HtmlText(tail));
      } else {
        htmlParts.add(new HtmlText(subsequentCode));
      }
    } else {
      htmlParts.add(new HtmlText(subsequentCode));
    }

    for (HtmlPart part in htmlParts) {
      part.printHtmlOn(buffer, context);
    }
  }

  Map toJson(JsonStrategy strategy) {
    return {
      'annotations': annotations
          .map((a) => strategy.encodeAnnotation(a))
          .toList(),
      'subsequentCode': subsequentCode,
    };
  }

  static CodePart fromJson(Map json, JsonStrategy strategy) {
    return CodePart(
      json['annotations'].map((j) => strategy.decodeAnnotation(j)).toList(),
      json['subsequentCode'],
    );
  }
}

class LineNumber extends HtmlPart {
  final int lineNo;
  final lineAnnotation;

  LineNumber(this.lineNo, this.lineAnnotation);

  @override
  HtmlPartKind get kind => HtmlPartKind.LINE_NUMBER;

  @override
  toJson(JsonStrategy strategy) {
    return {
      'kind': kind.index,
      'lineNo': lineNo,
      'lineAnnotation': strategy.encodeLineAnnotation(lineAnnotation),
    };
  }

  static LineNumber fromJson(Map json, JsonStrategy strategy) {
    return LineNumber(
      json['lineNo'],
      strategy.decodeLineAnnotation(json['lineAnnotation']),
    );
  }

  @override
  void printHtmlOn(StringBuffer buffer, HtmlPrintContext context) {
    buffer.write(
      lineNumber(
        lineNo,
        width: context.lineNoWidth,
        useNbsp: !context.usePre,
        className: context.getLineData(lineAnnotation).lineNumberClass,
      ),
    );
  }
}

class CodeLine extends HtmlPart {
  final Uri? uri;
  final int lineNo;
  final int offset;
  final StringBuffer codeBuffer = StringBuffer();
  final List<CodePart> codeParts = <CodePart>[];
  final List<Annotation> annotations = <Annotation>[];
  var lineAnnotation;

  CodeLine(this.lineNo, this.offset, {this.uri});

  @override
  HtmlPartKind get kind => HtmlPartKind.CODE;

  late final String code = codeBuffer.toString();

  @override
  void printHtmlOn(StringBuffer htmlBuffer, HtmlPrintContext context) {
    if (context.usePre) {
      LineData lineData = context.getLineData(lineAnnotation);
      htmlBuffer.write('<p class="${lineData.lineClass}">');
    }
    LineNumber(lineNo, lineAnnotation).printHtmlOn(htmlBuffer, context);
    for (CodePart part in codeParts) {
      part.printHtmlOn(htmlBuffer, context);
    }
    const NewLine().printHtmlOn(htmlBuffer, context);
    if (context.usePre) {
      htmlBuffer.write('</p>');
    }
  }

  @override
  Map toJson(JsonStrategy strategy) {
    return {
      'kind': kind.index,
      'lineNo': lineNo,
      'offset': offset,
      'code': code,
      'parts': codeParts.map((p) => p.toJson(strategy)).toList(),
      'annotations': annotations
          .map((a) => strategy.encodeAnnotation(a))
          .toList(),
      'lineAnnotation': lineAnnotation != null
          ? strategy.encodeLineAnnotation(lineAnnotation)
          : null,
    };
  }

  static CodeLine fromJson(Map json, JsonStrategy strategy) {
    CodeLine line = CodeLine(
      json['lineNo'],
      json['offset'],
      uri: json['uri'] != null ? Uri.parse(json['uri']) : null,
    );
    line.codeBuffer.write(json['code']);
    json['parts'].forEach(
      (part) => line.codeParts.add(CodePart.fromJson(part, strategy)),
    );
    json['annotations'].forEach(
      (a) => line.annotations.add(strategy.decodeAnnotation(a)),
    );
    line.lineAnnotation = json['lineAnnotation'] != null
        ? strategy.decodeLineAnnotation(json['lineAnnotation'])
        : null;
    return line;
  }
}

class JsonStrategy {
  const JsonStrategy();

  Map encodeAnnotation(Annotation annotation) {
    return {
      'id': annotation.id,
      'codeOffset': annotation.codeOffset,
      'title': annotation.title,
      'data': annotation.data,
    };
  }

  Annotation decodeAnnotation(Map json) {
    return Annotation(
      json['id'],
      json['codeOffset'],
      json['title'],
      data: json['data'],
    );
  }

  encodeLineAnnotation(lineAnnotation) => lineAnnotation;

  decodeLineAnnotation(json) => json;
}
